import os,sys
import django.utils.simplejson as json

from django.conf                        import settings
from django.contrib.auth.models         import Permission,Group,User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models        import Site
from django.core.exceptions             import ImproperlyConfigured,ObjectDoesNotExist
from django.db                          import transaction
from django.utils.datastructures        import SortedDict
from django.utils.encoding              import smart_unicode
from django.utils._os                   import safe_join
from sitepal.sitebase                   import models
from sitepal.sitebase.util              import iterappmodules,get_superuser

registered_handlers = SortedDict()
def register(handler):
    assert isinstance(handler,BaseSetup)
    if handler.name in registered_handlers:
        raise KeyError, "{0} is already registered".format(handler.name)
    registered_handlers[handler.name] = handler

class SetupError(Exception):
    pass

class BaseSetup(object):

    def __init__(self,name):
        self.name = name

    def __unicode__(self):
        return self.name

    def syncdb(self):
        raise NotImplementedError

class ListSetup(BaseSetup):
    def __init__(self,name,*handlers):
        BaseSetup.__init__(self,name)
        self.handlers = handlers
        self.errors   = None

    @transaction.commit_on_success
    def syncdb(self):
        try:
            self.errors = list()
            print map(lambda x: x.name,self.handlers)
            for handler in self.handlers:
                try:
                    handler.syncdb()
                except Exception,e:
                    if settings.DEBUG:
                        raise e
                    self.errors.append(str(e))
            if self.errors:
                raise SetupError(*self.errors)
        finally:
            self.errors = None

class ObjectSetup(BaseSetup):
    fs_encoding  = sys.getfilesystemencoding() or sys.getdefaultencoding()
    module_list  = None
    default_user = None

    def __init__(self,name,manager,save_on_init=False,allow_resync=False,name_attr=None):
        BaseSetup.__init__(self,name)
        self.manager        = manager
        self.file_name      = "{0}.json".format(self.name)
        self.file_list      = None
        self.errors         = None
        self.objects        = None
        self.synced_objects = None
        self.save_on_init   = bool(save_on_init)
        self.allow_resync   = bool(allow_resync)
        self.name_attr      = name_attr or 'name'

    def get_modules(self):
        return tuple(iterappmodules(submodule='siteapp.setup'))

    def get_file_list(self):
        file_list = []
        for app,mod in self.module_list:
            directory  = os.path.normpath(os.path.dirname(mod.__file__))
            setup_file = os.path.join(directory,self.file_name)
            if os.path.isfile(setup_file):
                file_list.append((app,setup_file.decode(self.fs_encoding)))
        return tuple(file_list)

    def get_default_user(self):
        return get_superuser()

    def get_object(self,name):
        if name not in self.synced_objects:
            data = self.objects[name]
            obj  = self.syncobject(name,data)
            obj.full_clean()
            obj.save()
            self.synced_objects[name] = (obj,data)
        return self.synced_objects[name][0]

    def get_content(self,data,value):
        if isinstance(data,dict) and '_app_conf_dir_' in data and value.startswith('#!'):
            file_name = safe_join(data['_app_conf_dir_'],value[2:])
            with open(file_name) as fp:
                return smart_unicode(fp.read())
        return smart_unicode(value)

    def initobject(self,name,data):
        assert isinstance(name,basestring) and len(name) > 0
        if name not in self.synced_objects:
            try:
                args = { "{0}__exact".format(self.name_attr): name }
                obj = self.manager.get(**args)
            except ObjectDoesNotExist:
                obj = self.manager.model()
                if isinstance(obj,models.Entity):
                    obj.site    = self.default_site
                    obj.owner   = self.default_user
                    obj.creator = self.default_user
                if isinstance(obj,models.Resource) or hasattr(obj,self.name_attr):
                    setattr(obj,self.name_attr,name)
            if isinstance(obj,models.Entity):
                obj.origin = data['_app_name_']
            if self.save_on_init:
                obj.save()
            self.synced_objects[name] = (obj,data)
            return obj
        return self.synced_objects[name][0]

    def syncobject(self,name,data):
        raise NotImplementedError

    def prepare_syncmeta(self):
        self.objects = dict()

    def cleanup_syncmeta(self):
        pass

    def syncmeta(self,obj,data):
        if 'object_acl' in data:
            ctype    = ContentType.objects.get_for_model(obj)
            ac_list  = data['object_acl']
            defaults = {
                'site'        : self.default_site,
                'grantee_type': ctype,
                'grantee_id'  : obj.pk,
                'granter'     : self.default_user
            }
            kwargs   = {
                'grantee_type__pk': ctype.id,
                'grantee_id'      : obj.pk,
                'site__pk'        : self.default_site.pk,
                'defaults'        : defaults
            }
            for ac_item in ac_list:
                if 'group' in ac_item and 'user' in ac_item:
                    raise ValueError,"Either 'user' or 'group' must be specified"
                defaults['inherits'] = bool(ac_item.get('inherits',False))
                for name in ac_item['permissions']:
                    perm = Permission.objects.get_by_natural_key(name,ctype.app_label,ctype.model)
                    defaults['permission']     = perm
                    kwargs  ['permission__pk'] = perm.pk
                    if 'group' in ac_item:
                        group = Group.objects.get(name__exact=ac_item['group'])
                        defaults['group']     = group
                        defaults['user']      = None
                        kwargs  ['group__pk'] = group.pk
                        kwargs  ['user__pk']  = None
                    else:
                        user = User.objects.get(username__exact=ac_item['user'])
                        defaults['group']     = None
                        defaults['user']      = user
                        kwargs  ['group__pk'] = None
                        kwargs  ['user__pk']  = user.pk
                    access,created = models.Access.objects.get_or_create(**kwargs)
                    if not created:
                        for attr,value in defaults.iteritems():
                            setattr(access,attr,value)
                        access.save()

    def syncobjects(self,objects):
        for name,data in objects.iteritems():
            if name in self.synced_objects and not self.allow_resync:
                continue
            try:
                obj = self.syncobject(name,data)
                obj.full_clean()
                obj.save()
                self.synced_objects[name] = (obj,data)
            except Exception,e:
                if settings.DEBUG:
                    print "{0} '{1}': {2}".format(self.name,name,str(e))
                    raise e
                self.errors.append("{0} '{1}': {2}".format(self.name,name,str(e)))

    def load_file(self,app_label,file_name):
        try:
            app_dir = os.path.dirname(file_name)
            with open(file_name) as fp:
                objects = json.load(fp)
            for name in objects.iterkeys():
                if isinstance(objects[name],basestring):
                    objects[name] = {'_app_conf_dir_': app_dir,
                                     '_app_name_'    : app_label,
                                     'content'       : objects[name]}
                elif isinstance(objects[name],(list,tuple)):
                    objects[name] = {'_app_conf_dir_': app_dir,
                                     '_app_name_'    : app_label,
                                     'items'         : objects[name]}
                elif isinstance(objects[name],dict):
                    objects[name]['_app_conf_dir_'] = app_dir
                    objects[name]['_app_name_']     = app_label
                else:
                    raise ValueError,"string, list or dict expected"
            return objects
        except Exception,e:
            if settings.DEBUG:
                raise e
            self.errors.append("file '{0}': {1}".format(file_name,str(e)))

    @transaction.commit_on_success
    def syncdb(self):
        self.default_site = Site.objects.get_current()
        if not self.module_list:
            self.module_list = self.get_modules()
        if not self.default_user:
            self.default_user = self.get_default_user()
        if not self.file_list:
            self.file_list = self.get_file_list()
        try:
            self.errors         = list()
            self.objects        = dict()
            self.synced_objects = SortedDict()
            for app_label,file_name in self.file_list:
                self.objects = self.load_file(app_label,file_name)
                self.syncobjects(self.objects)
            self.prepare_syncmeta()
            for name,value in self.synced_objects.iteritems():
                self.syncmeta(value[0],value[1])
            self.cleanup_syncmeta()
            try:
                self.mark_deprecated()
            except Exception,e:
                if settings.DEBUG:
                    raise e
                self.errors.append(str(e))
            if self.errors:
                raise SetupError(*self.errors)
        finally:
            self.errors         = None
            self.objects        = None
            self.synced_objects = None

    def mark_deprecated(self):
        if len(self.synced_objects) and issubclass(self.manager.model,models.Entity):
            app_names = map(lambda x: x[0],self.file_list)
            for obj in self.manager.filter(origin__in=app_names):
                if getattr(obj,self.name_attr) not in self.synced_objects:
                    print "saving on mark: '{0}'".format(getattr(obj,self.name_attr))
                    obj.origin = models.ORIGIN_UNKNOWN
                    obj.save()

